Skip to content

fix(types): refuse a bare exportOptions array on the object-grid zod mirror - #8485

Merged
os-sales merged 3 commits into
mainfrom
claude/issue-7762-export-options-bare-array-refusal
Sep 8, 2026
Merged

fix(types): refuse a bare exportOptions array on the object-grid zod mirror#8485
os-sales merged 3 commits into
mainfrom
claude/issue-7762-export-options-bare-array-refusal

Conversation

@claude

@claude claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #7762

Lane domain:spec. Clause-②: yes — this narrows a published accept set, so needs:contract-review is hung on this PR and it stays draft: not enqueued, no auto-merge, no self-approval. Parking green at the gate is the sanctioned resting state.

packages/plugin-grid/** is untouched. The routing ruling holds the renderer's read to be correct, and nothing in this change needed it to move.

What was wrong, measured on origin/main c90395b

ObjectGridSchema in packages/types/src/zod/objectql.zod.ts declared no exportOptions member, and BaseSchema is .passthrough(). Taken on a built tree:

'exportOptions' in ObjectGridSchema.shape        -> false
  live control    'bulkActions'                  -> true
  negative control 'zzNoSuchKey'                 -> false

safeParse({type:'object-grid', objectName:'accounts', exportOptions:['csv','xlsx']})
  -> success TRUE, data.exportOptions = ["csv","xlsx"]              VERBATIM
safeParse({... exportOptions:{formats:['csv','pdf'],compression:'gzip'}})
  -> success TRUE, returned VERBATIM

ObjectGrid.tsx reads schema.exportOptions?.formats and only that, and the effect that warns about dropped formats reads .formats too and returns early when it is absent, so the authored array then lost silently to the ['csv','json'] default with the export button still on screen. The card's premise re-measured green in full, against @objectstack/spec@17.3.0.

What this does

Declares the member as the spec's object arm, peeled out of SpecListViewSchema.shape.exportOptions and bound by reference, re-wrapped so its invalid_type message names the shape the renderer reads.

bare array ['csv','xlsx']                  -> ONE issue, code invalid_type, path ['exportOptions'],
                                              message naming { formats: [...] }
five-key object                            -> success TRUE, returned VERBATIM (streaming survives)
{ formats:['csv','pdf'] }                  -> refused, spec's own migration prescription
                                              ("'csv', 'xlsx' and 'json'", "os migrate meta")
{ formats:['csv'], compression:'gzip' }    -> refused, unrecognized_keys naming compression
node with no exportOptions                 -> unchanged, success TRUE

Not SpecListViewSchema.shape.exportOptions itself. That reference is a two-arm union whose first arm LIFTS a bare array to { formats } at parse; binding it would make this mirror accept-and-lift, the opposite of the ruled refusal. The sibling list-view mirror keeps that binding and keeps lifting — pinned as a live control in the new test.

Peeled rather than restated: a local copy of the five keys is a third copy of one contract, and the copy is what drifts. Every member schema is the spec's own object, asserted by toBe identity.

One correction worth recording, because it is invisible in the diff: the shape must be typed by the same two steps at the type level (unwrap, then the arm carrying a shape). Annotating it z.ZodRawShape compiles and builds, but collapses every member of the authoring face to unknown — measured, the drift ledger reported exportOptions NARROWER than declared. Recorded in the source comment so it is not reintroduced.

The re-derived ledger edit set, with its firing control

Every figure below was re-derived on current origin/main, symbol-located. Nothing was carried in from the card or any comment, and the previous round's addresses are all stale — PR #8354 rewrote that file after them.

Site (current origin/main) From To
UnmirroredDeclared entry objectql.zod.ts#ObjectGridSchema union, :1624 15 keys, 'exportOptions' among them 14 keys
SPEC_DERIVED_PAIRS, after objectql.zod.ts#ObjectGanttSchema absent added
header top-of-file, :153 14 entries / 87 keys 14 / 86
split half, :1404 SPEC-DERIVED (2 entries, 3 keys) (3 entries, 17 keys)
split half, :1457 LOCAL (12 entries, 84 keys) (11 entries, 69 keys)
totals line, :1491 14 entries / 87 keys — 2 / 3 spec-derived, 12 / 84 local 14 / 86 — 3 / 17, 11 / 69
UnmirroredDeclared docstring, :1300 87 keys is what this ledger records today 86
RuntimeOnlyDeclared docstring, :1695 UnmirroredDeclared records 87 keys today 86

Two figures the predecessor's enumeration did not have and one that changed shape: the pair also moves between the split's halves. Binding a Spec… symbol inside this mirror's initializer makes specReferencingExports re-derive the pair, so SPEC_DERIVED_PAIRS must gain it — the #7779 membership mechanism a second time. Measured, not reasoned: with the member added and the list untouched, SPEC_DERIVED_PAIRS matches what the mirror sources actually do failed with expected [ …(12) ] to deeply equal [ …(11) ]. The entry docblock records that the half is MEMBERSHIP and that the fourteen remaining keys are still ordinary local omissions, the reading #7279 kept on ObjectViewSchema for the same reason.

The two historical figures pinned at named revisions (beccf1c6b / ed7178bf3) are deliberately not touched — they cannot rot and must not chase the ledger.

The firing control. pnpm --filter @object-ui/types type-check is the instrument; vitest run …zod-mirror-parity.test.ts cannot see the type-level reconciliation at all, because the ledger is a TYPE map and vitest does not typecheck. Run TWICE, and both runs are reported because they measure different directions.

Leg A — on the pre-fix tree c90395b, the direction "a member appears". The mutation was the most generic member possible, exportOptions: z.any().optional():

CONTROL   clean tree, blob 4ddc342c… == that tree's blob   type-check -> exit 0, no diagnostics
ABLATION  one z.any() member injected, blob 5e201199…      type-check -> exit 2
          tsc's own line:
          src/__tests__/zod-mirror-parity.test.ts(2414,14): error TS2322:
            Type "objectql.zod.ts#ObjectGridSchema" is not assignable to type 'never'.
FALSE     the SAME mutated tree, vitest on that file       -> exit 0, "Tests 31 passed (31)"
GREEN
RESTORE   git checkout HEAD -- (the path); blob equality and an empty `git diff HEAD`,
          ⛔ not an exit code

⚠️ That :2414 is an address in the PRE-EDIT file, and it is not quoted as if it described this head: this PR's own ledger edits add lines above the assertion and move it. Leg B is the reading about the tree a reviewer checks out.

Leg B — re-run on head 1ee7f72d, the direction that applies now. The key is mirrored here, so the firing mutation is DELETING the member while the ledger row stays shrunk:

CONTROL   clean head 1ee7f72d, blob 37f6f962… == HEAD blob  type-check -> exit 0, no diagnostics
ABLATION  the member deleted, blob e41bea4e…                type-check -> exit 2
          tsc's own line:
          src/__tests__/zod-mirror-parity.test.ts(2444,14): error TS2322:
            Type "objectql.zod.ts#ObjectGridSchema" is not assignable to type 'never'.
RESTORE   blob back to 37f6f962…, `git diff HEAD` empty, ⛔ not an exit code

⚠️ On this head the same mutation ALSO reddens vitest — but through a DIFFERENT assertion, the runtime census SPEC_DERIVED_PAIRS matches what the mirror sources actually do (1 failed | 30 passed). The type-level reconciliation stays invisible to vitest: it is not one of that file's 31 runtime tests. The false-green warning is unchanged by vitest happening to fail here for another reason.

⚠️ One probe on this head returned a NULL reading and is reported rather than dropped: a member named zzLedgerProbe left type-check at exit 0. That is the probe testing the wrong direction — an UNDECLARED mirror key is not an UnmirroredDeclared ledger key — not evidence the instrument is asleep. Leg B is the valid control.

The new pin, and its ablation

packages/types/src/__tests__/object-grid-export-options-refusal-7762.test.ts — 11 tests: a non-vacuity floor that the spec arm is found with its five keys, shape membership with a live and a negative control, toBe identity per member, the "not the spec union" control (which also asserts list-view still lifts), the four authored readings, and three non-regression controls.

Ablated on the committed tree by deleting the member and running the pin: 6 of 11 red, and the 5 that stayed green are exactly the ones that must — the floor, the "not the union" control, the five-key object still accepted, the empty node, and the neighbouring members. Restored by git checkout HEAD -- the path, proved by blob equality (37f6f962…, this head) and an empty git diff HEAD.

Gates, derived from the changed-file set

Exit codes captured by redirect before any pipe.

Gate Verdict
pnpm --filter @object-ui/types build exit 0 — "dist completeness: 1 package(s) complete (124 emitted files verified)"
pnpm --filter @object-ui/types type-check exit 0, no diagnostics (the ledger's real instrument)
pnpm exec vitest run packages/types/ exit 0 — Test Files 145 passed, Tests 2766 passed
pnpm exec vitest run on the new pin exit 0 — Tests 11 passed
pnpm --filter @object-ui/types lint exit 0 — 0 errors, 265 pre-existing warnings
node scripts/check-changeset-presence.mjs exit 0 — "2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"
check-changeset-fixed / check-changeset-no-major exit 0 / exit 0
pnpm check:control-bytes exit 0 — 6711 tracked text files scanned
check:esm-specifiers check:spec-symbols check:self-import check:phantom-deps check:unreferenced-sources check:published-tsconfig-exclude check-type-check-coverage exit 0 each
check-governed-queue-guard --test on all four paths exit 0 — "NOT GOVERNED — 4 path(s) checked against 5 governed surface(s); none matched"
consumer tests touching exportOptions (the new pin, plugin-grid ×4, plugin-list, console parity) exit 0 — 7 files, 271 tests passed
downstream closure turbo run build --filter='...@object-ui/types' --filter='!@object-ui/site' --concurrency=2 exit 0 — 42 successful, 42 total

The downstream leg is the whole consumer closure, not a narrowing: @object-ui/types is the protocol package and this change moves its published z.input face, so every consumer was rebuilt (each package's build is tsc). Provenance: every figure in the table above was re-measured on head 1ee7f72d — the build, the type-check, the 145/2766 suite, the pin, the pin ablation, lint, all eleven check:* gates, the governed-surface probe, the 7-file consumer set and the 42-package closure. ⛔ Nothing here is a 29c0479 number relabelled with a newer sha. The two readings that are NOT from this head are labelled where they appear, and are readings about a different tree by design: the pre-fix baseline (taken on origin/main — that is the point of it) and firing-control Leg A (taken on c90395b, with Leg B re-run on this head). Heavy runs went through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-7762-objectui; verdicts read from its own VERDICT command-exit lines.

Console framework budget — merged main, and the bytes recovered

origin/main (61e649a) is merged in as 3ab8e40 (--no-ff, no conflicts, ⛔ no rebase / amend / force-push). The framework chunk is packages/(core|react|types), so this PR's published strings land in it.

Two legs, both built and measured locally:

tree framework gzip bytes vs the 71,000-byte ceiling
clean origin/main 61e649a 70,651 349 bytes of headroom, gate exits 0
this branch at 29c0479 (merged) 71,042 OVER by 42
this branch at 1ee7f72d 70,999 1 byte under, gate exits 0

⇒ The ceiling is not saturated: main is healthy and the 391-byte delta is this PR's. Two reductions were taken, each measured before choosing, and each pure redundancy removal that loses no fact:

  • the module-load throw message, 272 → 144 raw bytes — worth 35 gzip bytes. It still names the moved spec symbol, the member that depends on it and the card; the rationale it used to restate lives in the docblock above it, which the production minifier strips while the string ships.
  • two phrases in the shared guidance string, 378 → 363 raw bytes — worth 8 gzip bytes. "grid toolbar export menu" → "grid toolbar menu" after a sentence that already opens "Export configuration", and "would be dropped in silence" → "is silently dropped".
  • rejected: the peel's typeof cur.unwrap === 'function' predicate (an estimated 5–8 gzip bytes, deliberately not measured). A non-function truthy unwrap would then raise a TypeError instead of the diagnostic Error, which weakens the module-load guard.

PER_CHUNK_GZIP_CEILINGS.framework and PER_CHUNK_BASELINE are untouched — git diff origin/main -- scripts/check-eager-closure-budget.mjs is empty. Nothing was skipped, disabled or quarantined.

⚠️ The margin is ONE gzip byte, and that is a fact about the ceiling rather than about this PR. main itself only had 349 bytes of headroom on a chunk whose ceiling was last raised for an unrelated spec bump. The next card that adds any published string or schema member to packages/(core|react|types) hits this wall with nothing left to trim. Raising it is a maintainer-floor action and was not taken here; flagging it so the decision gets taken deliberately instead of discovered by the next lane.

The accept set did not move with the bytes. Verdict sets compared across the two blobs, same probe, rebuilt dist each time:

                pre-patch 7e1929d7…              post-patch 37f6f962…
bare array      invalid_type@exportOptions       invalid_type@exportOptions
['csv','pdf']   invalid_type@exportOptions       invalid_type@exportOptions
{formats:pdf}   invalid_value@exportOptions.formats.1   (identical)
five-key        ACCEPT                           ACCEPT
sixth key       unrecognized_keys@exportOptions  (identical)
no key          ACCEPT                           ACCEPT

Only the message LENGTH moved (376 → 361 characters). And the pin's own assertions re-verified on 1ee7f72d: /formats/ ✅, /list-view/ ✅, /object-grid/ ✅, and description === refusal message ✅ (both 363 UTF-8 bytes). list-view still LIFTS a bare array to { formats: ['csv','xlsx'] } and still refuses 'pdf' — no sideways leak. ObjectViewSchema.table.exportOptions moves with the change (it is ObjectGridSchema.omit(…).partial()): base accepted a bare array there, this head refuses it, the object form parses on both, and it has zero runtime readers — named in the changeset rather than left to be discovered.

⚠️ On object-grid the retired 'pdf' value is reachable only through the OBJECT spelling. The ARRAY spelling ['csv','pdf'] is refused earlier, by the named refusal, before the format enum is consulted — correct and intended, since the array form is not admitted on this node at all.

Fixture triage

Repo-wide sweep for an authored array spelling (git grep -n "exportOptions['\"]\?: *\["): four hits, none on an object-grid node — one changeset prose line, and three list-view fixtures (ListView.test.tsx, export-options-spec-parity.test.ts, p1-spec-alignment.test.ts) whose spelling is still legal there and is asserted to keep lifting. No fixture is owed a change.

验收备注

  • packages/plugin-grid/src/index.tsx's designer description for exportOptions still lists four keys and omits streaming. Noted, not filed: it is already recorded in the card body and routed by triage into its shard A table, and plugin-grid is read-only for this lane.
  • Triage's shard A (a ListView-style fold at ObjectGrid's read site) is REFUSED by the routing ruling, which holds the renderer's read correct. Recorded so a later reader does not resurrect it.
  • The ruling's cost argument, quoted verbatim and re-measured TRUE: 「收窄不弄坏任何今天能用的东西」 — the refused spelling was already a silent no-op, so the narrowing costs no working artefact.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE

Authored by Claude Code in session session_01CZY49skxUBYyJcdnTcYPrE (durable attribution in prose, so an edit does not accrete footer blocks).


Generated by Claude Code

…mirror

`ObjectGridSchema` declared no `exportOptions` member, and `BaseSchema` is
`.passthrough()`, so a bare format array validated green and came back verbatim
— then lost silently to the `['csv', 'json']` default, because `ObjectGrid`
reads `schema.exportOptions?.formats` and nothing else and the warning path
reads `.formats` too. The retired `'pdf'` value and a sixth key rode through the
same hole.

Declare the member as the spec's OBJECT arm, peeled out of
`ListViewSchema.shape.exportOptions` and bound by reference, so the bare array
is refused by name with a message pointing at the shape the renderer reads,
while `'pdf'` and a sixth key keep the spec's own messages. Not the spec union
itself: its first arm lifts a bare array to `{ formats }`, which is the opposite
of the refusal ruled here — the sibling `list-view` mirror keeps that binding
and keeps lifting.

The key leaves `UnmirroredDeclared` (87 -> 86 keys) and the spec reference
re-derives the pair into the split's SPEC-DERIVED half (2/3 -> 3/17
spec-derived, 12/84 -> 11/69 local); every pinned figure moved with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3473.9 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-CMQUv7Ja.js
Status FAIL

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.

Which half objected:

Eager-closure half Verdict
Aggregate closure ceiling ✅ pass
Per-chunk ceilings ❌ over its ceiling
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness (checkout vs. base branch) ✅ pass

📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 498.87KB 114.10KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 191.36KB 53.16KB
fields (index.js) 243.15KB 61.40KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.52KB 46.34KB
plugin-dashboard (index.js) 131.48KB 34.45KB
plugin-designer (index.js) 213.21KB 43.63KB
plugin-detail (index.js) 248.78KB 63.98KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.58KB 56.63KB
plugin-kanban (index.js) 55.40KB 15.71KB
plugin-list (index.js) 112.74KB 27.70KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.54KB 20.84KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

os-sales commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

⛔ CI red — Console Performance Budget, per-chunk half. Patch round in flight; ⛔ no ceiling will be raised here.

domain:spec @ objectui PM seat. Recording the blocker precisely so it is not re-diagnosed, and so nobody reads the red as a reason to widen a gate.

The measured breach, read from the Bundle Analysis job log at head 29c0479 — ⛔ not from the rendered comment, which names the failing half but not the chunk:

✅ aggregate eager closure   3473.9 KB / 3512.7 KB   (PASSES, headroom 38.8 KB)
❌ 1 eager chunk over its per-chunk budget:
   ✅ vendor-objectstack     1206.2 /  1224.6 KB
   ✅ i18n-locales            441.5 /   444.3 KB
   ✅ ui-components           384.0 /   389.6 KB
   ❌ framework                69.4 /    69.3 KB   (OVER by 0.0 KB)
✅ Ceiling sensitivity: pass          ✅ Ceiling freshness: pass

⇒ The aggregate is fine; one chunk is over by less than 50 bytes (it rounds to 0.0 KB).

The bytes are plausibly this PR's, and I am not pretending otherwise. apps/console/vite.config.ts:791 declares { name: 'framework', test: /[\\/]packages[\\/](core|react|types)[\\/]/ } — so the framework chunk is packages/types, which is the only shipped package this PR touches. The docblock is stripped by the console's production minifier, so the real additions are the peeled-shape IIFE, the module-load guard's message, and the shared guidance string.

⛔ What will NOT happen

What is in flight

A patch round on the same branch, in this order:

  1. Merge origin/main in and push — the PR is behind (base 7cf6f38f; main has moved to at least 61e649a). Owed regardless of this gate, and it re-runs the budget against current main. ⛔ No rebase, amend or force-push.
  2. Isolate whose bytes these are by measurement, not argument — build the console twice, origin/main clean vs the merged head, and report the framework gzip figure from each. The delta is this PR's true contribution.
  3. Only if the delta is genuinely this PR's, look for a reduction that costs the contract nothing.

⚠️ The reading that decides this, stated in advance so the answer is not fitted to a preference: the ceiling is 69.3 KB against a 69.4 KB measurement, i.e. roughly zero headroom. If step 2 shows main alone already sits at or above 69.3, then this is not this PR's defect at all — it is a saturated per-chunk ceiling, and because framework is packages/core|react|types, it would block essentially every card this lane has queued in packages/types. That outcome goes to the maintainer as a gate decision, ⛔ not into this PR.

PR stays draft, needs:contract-review stays hung, ⛔ not enqueued, ⛔ no auto-merge. The contract-review verdict is running separately and is unaffected by this.


Generated by Claude Code

…ork budget

The console's `framework` chunk is `packages/(core|react|types)`, and the
`exportOptions` refusal ships two string constants into it. Measured against a
clean `origin/main` build: `framework` was 70,651 gzip bytes with a 71,000 byte
ceiling (349 bytes of headroom), and this branch took it to 71,042 — over by 42.

Two reductions, each pure redundancy removal with no fact lost:

  * the module-load `throw` message, 272 -> 144 raw bytes, worth 35 gzip bytes.
    It still names the moved spec symbol, the member that depends on it and the
    card; the rationale it used to restate is in the docblock above it, which
    the production minifier strips while the string ships.
  * two redundant phrases in the shared guidance string, 378 -> 363 raw bytes,
    worth 8 gzip bytes ("grid toolbar export menu" -> "grid toolbar menu", after
    a sentence that already says "Export configuration"; "would be dropped in
    silence" -> "is silently dropped").

`framework` now measures 70,999 against the 71,000 ceiling. ⛔ No ceiling was
raised and no baseline moved. Every accept-set reading and every assertion of
the pin re-verified on this head: bare array still one invalid_type issue at
['exportOptions'] whose message still matches /formats/, /list-view/ and
/object-grid/ and is still identical to the member's description; the five-key
object still parses verbatim; 'pdf' and a sixth key still refused.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3473.8 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-OhyvhEOc.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 498.87KB 114.10KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 191.36KB 53.16KB
fields (index.js) 243.15KB 61.40KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.52KB 46.34KB
plugin-dashboard (index.js) 131.48KB 34.45KB
plugin-designer (index.js) 213.21KB 43.63KB
plugin-detail (index.js) 248.78KB 63.98KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.58KB 56.63KB
plugin-kanban (index.js) 55.40KB 15.71KB
plugin-list (index.js) 112.74KB 27.70KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.54KB 20.84KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sales
os-sales marked this pull request as ready for review September 8, 2026 03:10
@os-sales
os-sales added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit f08d1a8 Sep 8, 2026
34 checks passed
@os-sales
os-sales deleted the claude/issue-7762-export-options-bare-array-refusal branch September 8, 2026 03:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants